package org.carabiner.infinitest.swingui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.ToolTipManager;

import org.carabiner.infinitest.FilterList;
import org.carabiner.infinitest.InfinitestCore;
import org.carabiner.infinitest.InfinitestSettings;

/**
 * Swing based UI for Infinitest
 * 
 * @author <a href="mailto:benrady@gmail.com"Ben Rady</a>
 *
 */
public class InfinitestSwingGuiMain
{  
  private static final String APP_TITLE = "Infinitest";
  private InfinitestCore core;  
  private ResultTreeModel testResults;
  private JTree tree;
  private JFrame frame;
  private JProgressBar progressBar;    
  private long lastUpdate;
  private Timer titleTimer;

  public InfinitestSwingGuiMain(File filterListFile, File path) throws IOException
  {
    lastUpdate = System.currentTimeMillis();
    frame = new JFrame();    
    frame.setAlwaysOnTop(true);
    frame.setLayout(new BorderLayout());
    frame.addWindowListener(new WindowBoundsManager());
    testResults = new ResultTreeModel();
    tree = createTree();      
    frame.add(new JScrollPane(tree), BorderLayout.CENTER);
    progressBar = new JProgressBar();    
    progressBar.setForeground(Color.YELLOW);
    frame.add(progressBar, BorderLayout.SOUTH);
    testResults.addTreeModelListener(new TreeModelExpansionListener(tree));
    core = new InfinitestCore(filterListFile, true, path);
    core.addTestStatusListener(new EventNormalizer(testResults));
    initListeners();     
        
    frame.add(createToolBar(), BorderLayout.WEST);    
    frame.setBounds(WindowBoundsManager.getWindowBounds());
    titleTimer = createTitleTimer();
    updateGUI();
  }

  private Timer createTitleTimer()
  {
    Timer timer = new Timer(1000, new ActionListener()
    {
      public void actionPerformed(ActionEvent e)
      {
        updateTitle();
      }      
    });
    timer.start();
    return timer;
  }

  private Component createToolBar()
  {
    JToolBar toolbar = new JToolBar(JToolBar.VERTICAL);    
    toolbar.add(new ReloadIndexAction(core));    
    toolbar.add(new HaltTestAction(core));
    return toolbar;
  }

  private JTree createTree()
  {
    JTree resultsTree = new ResultsTree(testResults);
    resultsTree.setRootVisible(false);
    resultsTree.setEditable(false);
    resultsTree.setShowsRootHandles(true);
    resultsTree.setCellRenderer(new FailureCellRenderer());
    ToolTipManager tipManager = ToolTipManager.sharedInstance();
    tipManager.registerComponent(resultsTree);
    tipManager.setDismissDelay(10000);
    resultsTree.addMouseListener(new ClipboardCopyListener());
    return resultsTree;
  }
 
  JFrame getFrame()
  {
    return frame;
  }
  
  private void updateProgress(final int progress)
  {
    progressBar.setValue(progress);
    progressBar.setForeground(Color.YELLOW);    
  }
  
  private void updateGUI()
  {
    // If this gets any more complex, it should be refactored to the state
    // pattern.    
    updateTitle();
    lastUpdate = System.currentTimeMillis();
    if (core.shouldRunTests())
    {
      if (testResults.getTestCaseCount() == 0)
      {            
        progressBar.setForeground(InfinitestSettings.PASSING_COLOR);
      }
      else
      {       
        progressBar.setForeground(Color.RED);
      }
    }
    else
    {      
      progressBar.setForeground(Color.YELLOW);
    }
  }

  private void updateTitle()
  {
    String timeStamp = formatTime(System.currentTimeMillis() - lastUpdate);    
    frame.setTitle(APP_TITLE + " - " + core.getStatus() + " (" + timeStamp + ")");
  }
  
  public static void main(String[] args) throws IOException
  {
    File classFileDir = new File(args[0]);
    File filterListFile = null;
    if (args.length > 1)
      filterListFile = new File(args[1]);
    InfinitestSwingGuiMain app = new InfinitestSwingGuiMain(filterListFile, classFileDir);
    app.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);    
    app.frame.setVisible(true);
  }

  public FilterList getFilters()
  {
    return core.getFilters();
  }

  private void initListeners()
  {
    // A closure, a closure...my kingdom for a closure.
    core.addPropertyChangeListener(InfinitestCore.CURRENT_TEST_PROPERTY, new EventThreadChangeListener()
    {
      protected void handleEvent(PropertyChangeEvent evt)
      {
        frame.setTitle(APP_TITLE + " - Running Test: " + evt.getNewValue());
      }      
    });

    core.addPropertyChangeListener(InfinitestCore.RUN_TESTS_PROPERTY, new EventThreadChangeListener()
    {
      protected void handleEvent(PropertyChangeEvent evt)
      {
        Boolean runTests = (Boolean) evt.getNewValue();
        if (runTests)
          updateProgress(0);
        updateGUI();
      }      
    });
    
    core.addPropertyChangeListener(InfinitestCore.TESTS_IN_QUEUE_PROPERTY, new EventThreadChangeListener()
    {
      protected void handleEvent(PropertyChangeEvent evt)
      {
        if (evt.getOldValue().equals(0))
          progressBar.setMaximum((Integer)evt.getNewValue());
        int testsLeftToRun = progressBar.getMaximum() - (Integer)evt.getNewValue();
        updateProgress(testsLeftToRun);
        updateGUI();
      }      
    });
    
    core.addPropertyChangeListener(InfinitestCore.STATUS_PROPERTY, new EventThreadChangeListener()
    {
      protected void handleEvent(PropertyChangeEvent evt)
      {
        updateGUI();
      }
    });    
  }
  
  private static abstract class EventThreadChangeListener implements PropertyChangeListener
  {
    public void propertyChange(final PropertyChangeEvent evt)
    {
      Runnable runnable = new Runnable()
      {
        public void run()
        {
          EventThreadChangeListener.this.handleEvent(evt);
        }
      };
      runThisOnEventThread(runnable);
    }
    
    protected abstract void handleEvent(PropertyChangeEvent evt);        
    
    private static void runThisOnEventThread(Runnable runnable)
    {
      if (!SwingUtilities.isEventDispatchThread())
      {
        try
        {
          SwingUtilities.invokeAndWait(runnable);
        }
        catch (InterruptedException e)
        {
          throw new RuntimeException(e);
        }
        catch (InvocationTargetException e)
        {
          throw new RuntimeException(e);
        }
      }
      else
        runnable.run();
    }
  }

  /**
   * Takes an elapsed time (in milliseconds) and turns it
   * into a stopwatch style time
   * 
   * @param time
   * @return
   */
  static String formatTime(long time)
  {
    DateFormat dateFmt = new SimpleDateFormat("HH:mm:ss");
    dateFmt.setTimeZone(TimeZone.getTimeZone("GMT-0"));
    return dateFmt.format(new Date(time));    
  }

}
